# -*- coding: utf-8 -*-

from __future__ import with_statement

import datetime
import json
import logging
import os
import urllib
import webapp2

from google.appengine.api        import channel
from google.appengine.ext        import blobstore, db
from google.appengine.ext.webapp import template

from model.message import Message
from model.room    import Room, UserNotInRoom
from model.user    import User



##### Constants #####

# How URL is built - what are the names of GET arguments.
ROOM_URL_ARGUMENT = "room"
USER_URL_ARGUMENT = "user"

CHANNEL_TOKEN_TIMEOUT = 60   # In minutes.



##### Helper methods #####

def make_client_id(room, user):
    """Returns client id that indentifies it's channel."""
    
    return room.name + "/" + user.username


def identify_client(client_id):
    """
    Identifies the client by his id.
    
    Returns list of 3 arguments [valid_id, room, user]. valid_id is a boolean.
    If valid_id is False, provided client_id was invalid. If valid_id is True,
    room and user are Room and User instances corresponding to the client id.
    
    """
    valid_id = True
    try:
        room_name, username = client_id.split("/")
        room = Room.all().filter("name", room_name).filter("expired", False).get()
        user = User.all().filter("username", username).get()
        # If no room or no user with provided names exist, id is invalid.
        if (not room) or (not user):
            valid_id = False
    except (ValueError, AttributeError, db.BadValueError):
        valid_id = False
    
    return [valid_id, room, user]


def create_channel(room, user):
    """Creates a channel and returns it's token."""
    
    logging.debug("Creating channel in room %s for user %s" \
        % (room.key().id_or_name(), user.username))
    client_id = make_client_id(room, user)
    return channel.create_channel(client_id, CHANNEL_TOKEN_TIMEOUT)


def disconnect_user(room, disconecting_user):
    """Disconnecting user quits the room."""
    
    try:
        other_users = room.get_other_users(disconecting_user)
        # Remove sender from the room.
        room.remove_user(disconecting_user)
    except UserNotInRoom:
        logging.error("Invalid disconnection from user %s from room %s: user not in room" \
            % (disconecting_user.username, room.name))
        return
    
    room_expired = False
    if room.is_empty():
        # If room became empty, it expires.
        room.set_expired()
        room_expired = True   # To display log message.
    else:
        # Broadcast disconnection to other users.
        disconnection_message = { "type": "bye", "sender": disconecting_user.username }
        broadcast_message(room, other_users, disconnection_message)
    room.put()
    
    logging.info("User %s disconnected from room %s" \
        % (disconecting_user.username, room.name))
    if room_expired:
        logging.info("Empty room %s expired" % room.name)


def broadcast_message(room, to_users, message_obj):
    """Sends message to every user from to_users list."""
    
    message_json = json.dumps(message_obj)
    logging.debug("Brodcasting message %s in room %s" \
        % (message_json, room.name))
    for user_key in to_users:
        user = User.get(user_key)
        if user:
            send_message(room, user, message_json)
        else:
            logging.error("Brodcasting message error: user with key %s is in room %s but not in database" \
                % (user_key, room.name))

def send_message(room, to_user, message_json):
    channel.send_message(make_client_id(room, to_user), message_json)



#### Reqest handlers ####

class ChannelConnectPage(webapp2.RequestHandler):
    """User connection to channel."""
    
    def post(self):
        # Identify connecting client.        
        client_id = self.request.get("from")
        valid_id, room, user = identify_client(client_id)
        if not valid_id:
            logging.error("Invalid channel connection from client id %s: invalid client id" \
                % client_id)
            return
        
        try:
            # Set user connection to room.
            room.set_user_connection(user)
            room.put()
            logging.info("User %s connected to room %s" \
                % (user.username, room.name))
        except UserNotInRoom:
            logging.error("Invalid channel connection from user %s to room %s: user not in room" \
                % (user.username, room.name))


class ChannelDisconnectPage(webapp2.RequestHandler):
    """User channel closed."""
    
    def post(self):
        # Identify disconnecting client.
        client_id = self.request.get("from")
        valid_id, room, user = identify_client(client_id)
        
        # ID can be invalid, if after receiving "bye" message
        # from user room became empty and exipred.
        if valid_id and room.has_user(user):
            # User did not send "bye" message as he should.
            # We have to disconnect him here.
            logging.warning("User %s disconnecting from channel still in a room %s, though he should already be disconnected" %
                (user.username, room.name))
            disconnect_user(room, user)
                

class MessagePage(webapp2.RequestHandler):
    """User recieving a message."""
    
    def post(self):
        message_json    = self.request.body.decode('utf-8')
        room_name       = self.request.get(ROOM_URL_ARGUMENT)
        sender_username = self.request.get(USER_URL_ARGUMENT)
        
        try:
            message_obj = json.loads(message_json)
            room        = Room.all().filter("name", room_name).filter("expired", False).get()
            sender      = User.all().filter("username", sender_username).get()
            validation  = True
            if not (message_obj and room and sender):
                validation = False
        except (ValueError, db.BadValueError):
            validation  = False
        
        if not validation:
            logging.error("Recieved invalid message from user %s in room %s (message %s): validation failed" \
                % (sender_username, room_name, message_json))
            return
        
        if not room.has_user(sender):
            logging.error("Recieved invalid message from user %s in room %s (message %s): user not in room" \
                % (sender.username, room.name, message_json))
            return
        
        logging.debug("Recieved message from user %s in room %s (message %s)" \
            % (sender_username, room_name, message_json))
        self.handle_message(room, sender, message_obj)
    
    def handle_message(self, room, sender, message_obj):
        if not message_obj.has_key("type"):
            logging.error("Recieved invalid message from user %s in room %s (message %s): message has no type" \
                % (sender.username, room.name, json.dumps(message_obj)))
            return
        
        message_type = message_obj["type"]
        
        # User requests new token.
        if message_type == "tokenRequest":
            self.send_new_token(room, sender)
        # User quits the room.
        elif message_type == "bye":
            disconnect_user(room, sender)
        # Chat text message from user.
        elif message_type == "chatMessage":
            other_users = room.get_other_users(sender)
            broadcast_message(room, other_users, message_obj)
            
            content = db.Text(message_obj["content"])
            self.save_message(room, sender, other_users, content)            
        # Other message types.
        elif message_type in ["offer", "answer", "candidate", "fileUpload"]:
            if message_type == "offer":
                # Add sender username to the "offer" message.
                message_obj["sender"] = sender.username
            other_users = room.get_other_users(sender)
            broadcast_message(room, other_users, message_obj)
        else:
            logging.error("Recieved invalid message from user %s in room %s (message %s): unknown message type %s" \
                % (sender.username, room.name, json.dumps(message_obj), message_obj["type"]))
    
    def send_new_token(self, room, sender):
        """
        When sender requests new token, new channel is created and it's
        token send via the older one.
        
        """
        
        token = create_channel(room, sender)
        message_json = json.dumps({ "type": "tokenResponse", "token": token })
        send_message(room, sender, message_json)
    
    def save_message(self, room, sender, recipients, content):
        """Saves the message in datastore."""
        
        logging.debug("Saving message from %s in room %s: \"%s\"" \
            % (sender.username, room.name, content))
        new_message = Message(
            sender      = sender,
            recipients  = recipients,
            room        = room,
            content     = content,
            insert_time = datetime.datetime.now()
        )
        new_message.put()


class ChatPage(webapp2.RequestHandler):
    """Main chat window."""
    
    STUN_SERVER_URL = "stun.l.google.com:19302"
    
    def get(self):
        room_name = self.request.get(ROOM_URL_ARGUMENT)
        username  = self.request.get(USER_URL_ARGUMENT)
        room      = None
        user      = None
        if room_name and username:        
            room = Room.all().filter("name", room_name).filter("expired", False).get()
            user = User.all().filter("username", username).get()
        
        if (not room) or (not user):
            logging.error("Invalid chat request in room %s from user %s: validation failed" \
                % (room_name, username))
            self.redirect(webapp2.uri_for("main"))
            return
            
        if not room.has_user(user):
            logging.error("Invalid chat request in room %s from user %s: user not in room" \
                    % (room.name. user.username))
            self.redirect(webapp2.uri_for("login"))
            return
        
        logging.debug("Chat request in room %s from user %s" \
            % (room.name, user.username))
        
        token = create_channel(room, user)
        logging.debug("Token for user %s in room %s created: %s" \
            % (user.username, room.name, token))
        self.render_page(room, user, token)
    
    def make_peer_connection_config(self, stun_server, turn_server, turn_server_password):
        servers = []
        if turn_server:
            turn_config = "turn: {}".format(turn_server)
            servers.append({ "url": turn_config, "credential": turn_server_password })
        if stun_server:
            stun_config = "stun: {}".format(stun_server)
        else:
            stun_config = "stun:" + self.STUN_SERVER_URL
        servers.append({ "url": stun_config })
        return { "iceServers": servers }
    
    def make_media_constraints(self, hd_video):
        constraints = { "optional": [], "mandatory": {} }
        # Demo 16:9 video with media constraints.
        if hd_video.lower() == "true":
            # Demo with WHD by setting size with 1280x720.
            constraints["mandatory"]["minHeight"] = 720
            constraints["mandatory"]["minWidth"]  = 1280
            # Disabled for now due to weird stretching behavior on Mac.
            #else:
            # Demo with WVGA by setting Aspect Ration;
            #constraints["mandatory"]["maxAspectRatio"] = 1.778
            #constraints["mandatory"]["minAspectRatio"] = 1.777
        return constraints
    
    def build_send_message_url(self, room_name, username):
        """Returns URL to which the user submits message POST requests."""
        
        return webapp2.uri_for("chat_message") + "?" + urllib.urlencode({
            ROOM_URL_ARGUMENT: room_name,
            USER_URL_ARGUMENT: username
        })
    
    def render_page(self, room, user, token):
        stun_server = self.request.get("ss")
        turn_server = self.request.get("ts")
        hd_video    = self.request.get("hd")
        ts_pwd      = self.request.get("tp")
        peer_connection_config = self.make_peer_connection_config(stun_server, turn_server, ts_pwd)
        media_constraints =      self.make_media_constraints(hd_video)
        
        # caller==0 means that user is the one answering (recieving) a call.
        caller = 0
        if room.occupancy() > 1:
            caller = 1
        
        template_values = {
            "config": {
                "token"        : token,
                "token_timeout": CHANNEL_TOKEN_TIMEOUT * 60,
                "my_username"  : user.username.encode("utf-8"),
                "room_name"    : room.name.encode("utf-8"),
                "caller"       : caller,
                "peer_connection_config": peer_connection_config,
                "media_constraints"     : media_constraints
            },
            "routes": {
                "send_message_url"    : self.build_send_message_url(room.name, user.username),
                "file_upload_url"     : blobstore.create_upload_url(webapp2.uri_for("file_upload")),
                "file_upload_form_url": webapp2.uri_for("file_upload_form"),
                # A little awkward (but the only one I could find) method of
                # obtaining file_download URL without providing file_key
                # argument.
                "file_download_url"   : webapp2.uri_for("file_download", blob_key="a")[:-1],
                "history_url"         : webapp2.uri_for("history", username=user.username),
                "main_url"            : webapp2.uri_for("main"),
                "room_url_argument"   : ROOM_URL_ARGUMENT,
                "user_url_argument"   : USER_URL_ARGUMENT
            }
        }
        template_path = os.path.join(os.path.dirname(__file__), "../templates/chat.html")
        self.response.out.write(template.render(template_path, template_values))